package de.stefanocke.roo.wicket.model;

import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.Service;
import org.springframework.roo.classpath.PhysicalTypeMetadata;
import org.springframework.roo.classpath.TypeManagementService;
import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails;
import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetailsBuilder;
import org.springframework.roo.classpath.details.FieldMetadata;
import org.springframework.roo.classpath.details.FieldMetadataBuilder;
import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder;
import org.springframework.roo.classpath.itd.ItdTypeDetailsProvidingMetadataItem;
import org.springframework.roo.metadata.MetadataItem;
import org.springframework.roo.model.JavaSymbolName;
import org.springframework.roo.model.JavaType;
import org.springframework.roo.support.logging.HandlerUtils;

import de.stefanocke.roo.wicket.RooProperties;
import de.stefanocke.roo.wicket.RooProperty;
import de.stefanocke.roo.wicket.RoundTripMode;
import de.stefanocke.roo.wicket.Stereotypes;
import de.stefanocke.roo.wicket.base.ModelClassAwareItdMetadataProviderBase;
import de.stefanocke.roo.wicket.base.ModelClassAwareMetadataCreationParams;

@Component
@Service
/**
 * This metadata provider creates constants in the governor class
 * which represents the bean properties. A form or details component typically
 * uses such BeanPropertiesMetadata, to determine, which fields to render.
 * The fields are based on the properties / fields of the referenced modelClass.
 * In opposite to other metadata, no ITD is used, but the constants
 * are directly rendered into the governor. This allows to user to place
 * additional annotations on them.
 * All those constants get the RooProperty annotation to allow Roo to find them.
 * 
 * 
 * @author stoc
 *
 */
public class BeanPropertiesMetadataProvider extends
		ModelClassAwareItdMetadataProviderBase<BeanPropertiesMetadata, BeanPropertiesAnnotationValues> implements
		IBeanPropertiesMetadataProvider {

	protected static final Logger logger = HandlerUtils.getLogger(BeanPropertiesMetadataProvider.class);

	@Reference
	private TypeManagementService typeManagementService;

	public BeanPropertiesMetadataProvider() {
		super(RooProperties.class, BeanPropertiesMetadata.class, BeanPropertiesAnnotationValues.class);
	}

	@Override
	protected ItdTypeDetailsProvidingMetadataItem getMetadataInternal(
			ModelClassAwareMetadataCreationParams<BeanPropertiesAnnotationValues> cmcp) {

		List<BeanPropertyMetadata> scaffoldEligibleProperties = modelClassService.getScaffoldEligibleProperties(cmcp
				.getAnnotationValues().getModelClass(), cmcp.getMid());
		// make mutable !?
		scaffoldEligibleProperties = createOrUpdateFieldsAndProjectAndEnrichPropertyMetadata(cmcp,
				scaffoldEligibleProperties);

		return new BeanPropertiesMetadata(cmcp, scaffoldEligibleProperties);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public List<BeanPropertyMetadata> getBeanPropertiesForModelClass(JavaType modelClass, JavaType beanPropertiesClass, String mid,
			boolean withExcludedFields) {
		
		BeanPropertiesMetadata mcpm = findWicketMetadata(mid, modelClass, BeanPropertiesMetadata.class, beanPropertiesClass, true);	
		return mcpm.getProperties(withExcludedFields);
		
	
	}

	/**
	 * To provide the user a place where to put Roo-Wicket specific annotations
	 * on model class fields, we create a static final string field for each
	 * property.
	 * 
	 * @param governorPhysicalTypeMetadata
	 * @param propertyMetadata
	 * @param roundTripMode
	 * @return
	 */
	private List<BeanPropertyMetadata> createOrUpdateFieldsAndProjectAndEnrichPropertyMetadata(
			ModelClassAwareMetadataCreationParams<BeanPropertiesAnnotationValues> cmcp,
			List<BeanPropertyMetadata> propertyMetadata) {

		PhysicalTypeMetadata projectionPhysTypeMetadata = cmcp.getGovernorPhysicalTypeMetadata();
		ClassOrInterfaceTypeDetails projectionCid = (ClassOrInterfaceTypeDetails) projectionPhysTypeMetadata
				.getMemberHoldingTypeDetails();
		ClassOrInterfaceTypeDetailsBuilder projectionBuilder = new ClassOrInterfaceTypeDetailsBuilder(projectionCid);

		boolean changed = false;

		Map<JavaSymbolName, BeanPropertyMetadata> fieldNameToMetadata = new HashMap<JavaSymbolName, BeanPropertyMetadata>();
		for (BeanPropertyMetadata pm : propertyMetadata) {
			fieldNameToMetadata.put(new JavaSymbolName(pm.getFieldConstantName()), pm);
		}

		RoundTripMode roundTrip = cmcp.getAnnotationValues().getRoundTrip();

		// Add missing projected fields, if they are not there yet and if new
		// fields shall be added automatically.

		if (roundTrip == RoundTripMode.FULL || roundTrip == RoundTripMode.ADD_ONLY) {
			for (JavaSymbolName fieldName : fieldNameToMetadata.keySet()) {
				BeanPropertyMetadata pm = fieldNameToMetadata.get(fieldName);
				String propertyName = pm.getPropertyName().getSymbolName();

				FieldMetadata field = projectionCid.getField(fieldName);
				if (field == null) {
					field = createField(projectionBuilder, propertyName, fieldName);
					changed = true;
				}
			}
		}

		// Delete superfluous projected fields if requested
		if (roundTrip == RoundTripMode.FULL || roundTrip == RoundTripMode.REMOVE_ONLY) {
			List<FieldMetadataBuilder> declaredFields = projectionBuilder.getDeclaredFields();
			Iterator<FieldMetadataBuilder> fieldsIterator = declaredFields.listIterator();
			while (fieldsIterator.hasNext()) {
				FieldMetadataBuilder fieldMetadata = (FieldMetadataBuilder) fieldsIterator.next();
				AnnotationMetadataBuilder fieldAnnotation = fieldMetadata.getDeclaredTypeAnnotation(BeanPropertyMetadata.ROOPROPERTY);
				if (fieldAnnotation != null) {
					JavaSymbolName fieldName = fieldMetadata.getFieldName();
					if (!fieldNameToMetadata.containsKey(fieldName)) {
						// Delete the field. But only, if the user has not
						// applied
						// any changes. otherwise, add @Deprecated
						if (fieldMetadata.getAnnotations().size() == 1 && fieldAnnotation.getAttributes().isEmpty()) {
							fieldsIterator.remove();
							changed = true;
						} else {
							// Mark the field as Deprecated
							if (fieldMetadata.getDeclaredTypeAnnotation(new JavaType(Deprecated.class)) == null) {
								fieldMetadata.addAnnotation(new AnnotationMetadataBuilder(Deprecated.class));
								changed = true;
							}
						}

					}
				}
			}
		}

		if (changed) {
			typeManagementService.createOrUpdateTypeOnDisk(projectionBuilder.build());
			// force recreation of governorPhysicalTypeMetadata

			PhysicalTypeMetadata refreshedPhysicalTypeMetadata = (PhysicalTypeMetadata) metadataService
					.evictAndGet(cmcp.getGovernorPhysicalTypeMetadata().getId());
			cmcp.getCommonMetadataCreationParams().setGovernorPhysicalTypeMetadata(refreshedPhysicalTypeMetadata);
			projectionCid = (ClassOrInterfaceTypeDetails) refreshedPhysicalTypeMetadata.getMemberHoldingTypeDetails();
		}

		// create the resulting list of properties, in the order as determined
		// by the projection
		List<BeanPropertyMetadata> resultingPropertyMetadata = new ArrayList<BeanPropertyMetadata>();
		// TODO: Support "projection superclasses" here?
		List<? extends FieldMetadata> projectedFields = projectionCid.getDeclaredFields();
		for (FieldMetadata projectedField : projectedFields) {
			BeanPropertyMetadata beanPropertyMetadata = fieldNameToMetadata.get(projectedField.getFieldName());
			if (beanPropertyMetadata != null && projectedField.getAnnotation(BeanPropertyMetadata.ROOPROPERTY) != null) {
				// we enrich the original field's annotations with the projected
				// field's annotations
				beanPropertyMetadata.addAnnotations(projectedField.getAnnotations());
				beanPropertyMetadata.setFieldConstantGovernor(cmcp.getGovernorPhysicalTypeMetadata().getType());
				resultingPropertyMetadata.add(beanPropertyMetadata);
			}

		}
		return resultingPropertyMetadata;
	}

	// private boolean hasNoAttributes(AnnotationMetadataBuilder
	// fieldAnnotation) {
	// for (JavaSymbolName name : fieldAnnotation.getAttributeNames()) {
	// if (fieldAnnotation.getAttribute(name) != null)
	// return false;
	// }
	// return true;
	// }

	private FieldMetadata createField(ClassOrInterfaceTypeDetailsBuilder builder, String propertyName,
			JavaSymbolName fieldName) {
		// There is no string constant yet for the field. Create one and give it
		// @RooField annotation.
		AnnotationMetadataBuilder amb = new AnnotationMetadataBuilder(BeanPropertyMetadata.ROOPROPERTY);
		FieldMetadataBuilder fmb = new FieldMetadataBuilder(builder.getDeclaredByMetadataId(), Modifier.PUBLIC
				| Modifier.FINAL | Modifier.STATIC, fieldName, new JavaType("java.lang.String"), "\"" + propertyName
				+ "\"");
		fmb.addAnnotation(amb);
		FieldMetadata fieldMetadata = fmb.build();
		builder.addField(fieldMetadata);
		return fieldMetadata;
	}

	@Override
	protected void notifyIfRequired(MetadataItem metadataItem) {
		// int newHash = metadataItem.hashCode();
		// logger.warning("Hash for "+metadataItem.getId()+" : "+newHash);
		// logger.warning("ToString for "+metadataItem.getId()+" : "+metadataItem.toString());
		super.notifyIfRequired(metadataItem);
	}

	@Override
	protected String requiredStereotype() {
		return Stereotypes.ANNOTATION_UTILS;
	}
}
